﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace MipsMachine
{
	public enum CompilerLogEntryType
	{
		Information,
		Warning,
		Error
	}
	public class CompilerLogEntry
	{
		private string message;
		private string filename;
		private int line;
		private int character;
		private CompilerLogEntryType type;
		public string Message { get { return message; } }
		public string Filename { get { return filename; } }
		public int Line { get { return line; } }
		public int Character { get { return character; } }
		public CompilerLogEntryType Type { get { return type; } }
		public CompilerLogEntry(string msg, string fn, int ln, int ch, CompilerLogEntryType tp)
		{
			message = msg;
			filename = fn;
			line = ln;
			character = ch;
			type = tp;
		}
		public override string ToString()
		{
			return type + ": " + message + " at line " + line + " column " + character + " in file " + filename;
		}
	}
	public class SyntaxErrorException : Exception
	{
		private int line, character;
		public SyntaxErrorException(string message, int line, int character)
			: base(message)
		{
			this.line = line;
			this.character = character;
		}
	}

	public enum Endianness
	{
		big,
		small
	}

	public class MipsAssembler
	{
		public const string TokenizerRegex = 
@"(?<Comment>(//.*$|;.*$|--.*$|\#.*$))|
(?<NewLine>\r\n|\n)|
^\s*(?<Label>[a-zA-Z_][a-zA-Z0-9_]*:)|
(\#(?<Preprocessor>[a-zA-Z]+))|
(?<String>"".*""|'.*')|
(?<Identifier>[a-zA-Z\._][a-zA-Z\._0-9]*)|
(?<Register>\$([0-9][0-9]?|zero|at|v0|v1|a[0-3]|t[0-9]|s[0-7]|k0|k1|gp|sp|fp|ra))|
(?<MemRegister>\(\s*\$([0-9][0-9]?|zero|at|v0|v1|a[0-3]|t[0-9]|s[0-7]|k0|k1|gp|sp|fp|ra)\s*\))|
,|
(?<WhiteSpace>\u0020+|\t+)|
(?<Number>(0x[0-9a-fA-F]+)|(-?[0-9]+))";
		public static readonly Regex Tokenizer = new Regex(
			TokenizerRegex, RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace | RegexOptions.CultureInvariant | RegexOptions.Multiline | RegexOptions.IgnoreCase);
		private Dictionary<string, string> compiler_options;
		public static readonly Regex StringRegex = new Regex(
			"\"(?<Value>[^\"]*)\"|'(?<Value>[^'])'", RegexOptions.Compiled);
		public static readonly Regex HexRegex = new Regex(
			"0x(?<Value>[0-9a-fA-F]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
		public static readonly Regex BinRegex = new Regex("b(?<Value>[0-1]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
		public static readonly Regex DecRegex = new Regex("(?<Value>[0-9]+)", RegexOptions.Compiled);
		public static readonly Regex OctRegex = new Regex("c(?<Value>[0-7]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase);

		public Dictionary<string, string> CompilerOptions { get { return compiler_options; } }
		private List<string> search_paths;
		public List<string> SearchPaths { get { return search_paths; } }
		public class FilePair
		{
			private string filename;
			private string content;
			public string Filename { get { return filename; } }
			public string Comtent { get { return content; } }
			public FilePair(string fn, string cont)
			{
				filename = fn;
				content = cont;


			}
			public FilePair(string fn, IEnumerable<string> search_paths)
			{
				foreach (var item in search_paths)
				{
					string fname = System.IO.Path.Combine(item, fn);
					if (System.IO.File.Exists(fname))
					{
						filename = fname;
						content = System.IO.File.ReadAllText(fname);
						return;
					}
				}
				throw new System.IO.FileNotFoundException("Unable to locate \"" + fn + "\" in search path.");
			}
		}

		public MipsAssembler()
		{
			search_paths = new List<string>();

			compiler_options = new Dictionary<string, string>();
			// Enduanness. Supported values:
			//  big - Use big endian (default)
			//  little/small - Use little endian
			compiler_options.Add("endian", "big");
			// Assembly flavor. Supported values:
			//  att - Use AT&T syntax (default)
			//  intel - Use Intel Syntax
			compiler_options.Add("flavor", "att");
			// Macro assembler
			// yes -Macro extensions enabled
			// no - Macro extensions disabled (default)
			compiler_options.Add("use_macro", "no");
		}
		/// <summary>
		/// Assembles a list of source files
		/// </summary>
		/// <param name="files">A key-value list where Key is filename, and value is code</param>
		/// <param name="ret">The binary assembled program</param>
		/// <returns>Assembler log</returns>
		public void Assemble(IEnumerable<FilePair> files,Action<string, int> progress, out MipsProgram ret, Action<CompilerLogEntry> callback)
		{
			List<MipsMetaInstruction> meta = new List<MipsMetaInstruction>();
			foreach (var item in files)
			{
				Assemble(item, progress, callback, meta);
			}
			ret = Compile(meta, callback);
		}
		/// <summary>
		/// Assembles a single source file
		/// </summary>
		/// <param name="code">Code contained in filename</param>
		/// <param name="filename">Filename used by log</param>
		/// <param name="ret">Return program</param>
		/// <returns>Assembler log</returns>
		public void Assemble(string code, Action<string, int> progress, string filename, out MipsProgram ret, Action<CompilerLogEntry> callback)
		{
			List<MipsMetaInstruction> meta = new List<MipsMetaInstruction>();
			bool found_error = false;
			Assemble(new FilePair(code, filename), progress, callback + (item => { if (item.Type == CompilerLogEntryType.Error) found_error = true; }), meta);
			if (found_error)
				ret = null;
			else
				ret = Compile(meta, callback);
		}

		private void ConvertData(List<MipsMetaInstruction> meta_program, Function funct, string constant, ref string line_label,  Action<string, CompilerLogEntryType> callback)
		{
			try
			{
				meta_program.AddRange(CreateDataInstructions(funct, constant, line_label, ""));
			}
			catch (NotImplementedException)
			{
				callback("Invalid data decleration",  CompilerLogEntryType.Error);
			}
			catch (SyntaxErrorException ex)
			{
				callback(ex.Message, CompilerLogEntryType.Error);
			}

			line_label = "";
		}

		/// <summary>
		/// Assembles source code into a binary executable
		/// </summary>
		/// <param name="code">Assembly code to compile</param>
		/// <returns>MIPS program</returns>
		protected void Assemble(FilePair item, Action<string, int> progress, Action<CompilerLogEntry> callback, List<MipsMetaInstruction> meta_program)
		{
			string constant = "";
			int argument = 0;
			string[] registers = new string[3];
			string line_label = "";
			bool found_opcode = false;
			OpCode opcode = OpCode.Undefined;
			Function funct = Function.SLL;
			List<string> args = new List<string>();
			var m = Tokenizer.Match(item.Comtent);
			int line_index = 1;
			int last_newline = 0;
			string preprocessor = "";
			if(progress != null)
			progress(item.Filename, 0);
			
			
			//
			// Following is functions that are used one or more times in the following code
			// Delegates and full closures are used as a replacment for functions in functions.
			//
			#region Inline functions

			// Emits an error if a callback routine is specified
			Action<string, CompilerLogEntryType> EmitCallback;
			if (callback != null)
				EmitCallback = (msg, type) =>
				{
					callback(new CompilerLogEntry(msg, item.Filename, line_index, m.Index - last_newline, type));
				};
			else
				EmitCallback = (msg, type) => { };

			Action<Function> SetDataOpcode = (type) =>
			{
				opcode = OpCode.Data;
				funct = type;
				found_opcode = true;
			};

			// Resets the state machine
			Action Reset = () =>
				{
					
					preprocessor = "";
					args.Clear();
					found_opcode = false;
					registers = new string[3];
					opcode = OpCode.Undefined;
					funct = Function.SLL;
					constant = "";
					argument = 0;
				};
			// Emits instructions from the current data gathered
			Action EmitInstruction = () =>
			{
				if (string.IsNullOrEmpty(preprocessor))
				{
					// If opcode is defined, we need to add a metainstruction
					if (found_opcode)
					{
						// Opcode is a data pseudoinstruction
						if (opcode == OpCode.Data)
						{
							ConvertData(meta_program, funct, constant, ref line_label, EmitCallback);
						}
						else
						{
							switch (funct)
							{

								case Function.LD:
									// Load 32-bit from address $s
									MipsMetaInstruction ins;
									ins = new MipsMetaInstruction(OpCode.LW, Function.SLL, line_label, registers[0], registers[1], registers[2], constant, line_index);
									ins = new MipsMetaInstruction(OpCode.LW, Function.SLL, line_label, registers[0], registers[1], registers[2], constant, line_index);
									break;
								case Function.LID:
									// Load 32-bit from immediate to $t
									break;
								case Function.SD:
									// Store 32-bit to address $s
									break;
								case Function.SID:
									// Store 32-bit immediate to address $s
									break;
								default:
									meta_program.Add(new MipsMetaInstruction(opcode, funct, line_label, registers[0], registers[1], registers[2], constant, line_index));
									break;
							}
							line_label = "";
						}
					}
				}
				else
				{
					switch (preprocessor)
					{
						case "include":
							EmitCallback("Including file '" + args[0] + "'", CompilerLogEntryType.Information);
							try
							{
								Assemble(new FilePair(args[0], search_paths), progress, callback, meta_program);
							}
							catch (System.IO.FileNotFoundException)
							{
								EmitCallback("Could not locate file \"" + args[0] + "\" in search paths",  CompilerLogEntryType.Error);
							}
							break;
					}

				}
			};
			#endregion



			while (m.Success)
			{
				// We want to ignore whitespace

				if (!m.Groups["WhiteSpace"].Success)
				{
					// Token is a label (my_label:)
					if (m.Groups["Label"].Success)
					{
						if (string.IsNullOrEmpty(line_label))
							line_label = m.Value.Substring(0, m.Value.Length - 1); // Omit :
						else
							EmitCallback("This line is already labeled",  CompilerLogEntryType.Error);
					}
					// Token is an identifier, such as an instruction or constant
					else if (m.Groups["Identifier"].Success)
					{
						if (string.IsNullOrEmpty(preprocessor))
						{

							if (!found_opcode)
							{
								string s = m.Groups["Identifier"].Value.ToLower();
                                // Pseudo instructions for data
								if (s == "db" || s == "data")
									SetDataOpcode(Function.DataByte);
								else if (s == "dw")
									SetDataOpcode(Function.DataWord);
								else if (s == "dd")
									SetDataOpcode(Function.DataDword);
								else
								{
									try
									{
										OpCode op = (OpCode)Enum.Parse(typeof(OpCode), s, true);
										opcode = op;
										found_opcode = true;
									}
									catch (ArgumentException)
									{
										// Parsing failed, this is likely a R-Type instruction
										try
										{
											// Parse opcode as a Funct
											Function fn = (Function)Enum.Parse(typeof(Function), s, true);
											funct = fn;
											opcode = OpCode.Undefined;
											found_opcode = true;
										}
										catch (ArgumentException)
										{
											EmitCallback("Unrecognized opcode '" + m.Groups["Identifier"].Value + "'",  CompilerLogEntryType.Error);
										}
									}
								}
							}
							else if (string.IsNullOrEmpty(constant))
								constant = m.Value;
							else
								EmitCallback("Unexpected identifier '" + m.Value + "'",  CompilerLogEntryType.Error);
						}
						else
						{
							if (argument == args.Count)
								args.Add(m.Value);
							else 
								EmitCallback("Syntax error, missing argument seperator",  CompilerLogEntryType.Error);
						}

					}
						// Token is a memory register C($R)
					else if (m.Groups["MemRegister"].Success)
					{
						registers[1] = m.Groups["MemRegister"].Value.Trim('(', ')').Trim();
					}
						// Token is a string "C"
					else if (m.Groups["String"].Success)
					{
						if (opcode == OpCode.Data)
							constant = m.Value;
						else if (argument == args.Count)
							args.Add(m.Value.Trim('\'', '"'));
						else
							EmitCallback("Missing argument seperator",  CompilerLogEntryType.Error);
					}
					// Token is a number, either hexadecimal or numeric
						// 0xC or C
					else if (m.Groups["Number"].Success)
					{
						if (string.IsNullOrEmpty(preprocessor))
						{
                            if (string.IsNullOrEmpty(constant) && found_opcode)
                            {
								MipsInstruction ins = new MipsInstruction()
								{
									OpCode = opcode,
									Funct = funct
								};
                                //if (ins.Type == InstructionType.IType || ins.Type == InstructionType.JType 	)
                                constant = m.Value;
                                //else
                                    //EmitCallback("Unexpected constant. Not an I-Type instruction.", CompilerLogEntryType.Error);
                                    
                            }
                            else 
                                EmitCallback("Unexpected constant", CompilerLogEntryType.Error);
						}
						else
						{
							args.Add(m.Value);
						}
					}
					// Token is a register
						// $R or $C
					else if (m.Groups["Register"].Success)
					{
						if (!found_opcode)
							EmitCallback("Unexpected identifier", CompilerLogEntryType.Error);
						else if (argument < registers.Length && string.IsNullOrEmpty(registers[argument]))
							registers[argument] = m.Value;
						else 
							EmitCallback("Missing ',' before argument '" + m.Value + "'", CompilerLogEntryType.Error);
					}
					// Token is a preprocessor directive
						// #STRING
					else if (m.Groups["Preprocessor"].Success)
					{
						if (string.IsNullOrEmpty(preprocessor))
							preprocessor = m.Groups["Preprocessor"].Value;
						else
							EmitCallback("Multiple preprocessor directives are not allowed on the same line", CompilerLogEntryType.Error);

					}
					// Token is an argument seperator
						// ,
					else if (m.Value == ",")
						argument++;
					// Token is a newline
						// \r\n or \n
					else if (m.Groups["NewLine"].Success)
					{
						EmitInstruction();
						Reset();
						last_newline = m.Index + m.Length;
						line_index++;
					}
				}
				// Move to next token
				m = m.NextMatch();
				if(progress != null)
					progress(item.Filename, (int)((m.Index / (float)item.Comtent.Length) * 100));
			}
			EmitInstruction();

		}
		private static readonly Regex EscapeRegex = new Regex(
			@"\\r|\\n|\\t|\\a|\\b|\\v|\\f|\\0|\\\\", RegexOptions.Compiled);

		public static string EscapeString(string input)
		{
			return EscapeRegex.Replace(input, item =>
			{
				switch (item.Value)
				{
					case @"\r":
						return "\r";
					case @"\n":
						return "\n";
					case @"\t":
						return "\t";
					case @"\f":
						return "\f";
					case @"\a":
						return "\a";
					case @"\b":
						return "\b";
					case @"\v":
						return "\v";
					case @"\0":
						return "\0";
					case @"\\":
						return "\\";
					default:
						return item.Value;
				}
			});
		}

		protected IEnumerable<MipsMetaInstruction> CreateDataInstructions(Function datatype, string immediate, string label, string filename)
		{
			var m = StringRegex.Match(immediate);
			string s = "";
			Func<byte[], MipsMetaInstruction> EmitInstruction = (dt) =>
			{
				MipsMetaInstruction ins = new MipsMetaInstruction(OpCode.Data, datatype, "", "", "", "", "", 0);
				ins.Label = label;
				ins.RawData = dt;
				label = "";
				return ins;
			};
			if (m.Success)
			{
				s = EscapeString(m.Groups["Value"].Value);
				byte[] data;
				switch (datatype)
				{
					case Function.DataByte:
						data = Encoding.ASCII.GetBytes(s);
						break;
					case Function.DataWord:
						data = Encoding.Unicode.GetBytes(s);
						break;
					case Function.DataDword:
						data = Encoding.UTF32.GetBytes(s);
						break;
					default:
						goto case Function.DataByte;
				}

				int count = data.Length;
				byte[] raw = new byte[4];
				int i;

				for (i = 0; i < count; i++)
				{
					if (i % 4 == 0)
						raw = new byte[4];
					raw[i % 4] = data[i];
					if (i % 4 == 3)
					{
						yield return EmitInstruction(raw);
					}
				}
				if (i % 4 < 3 && i % 4 > 0)
				{
					yield return EmitInstruction(raw);
				}
			}
			else if ((m = HexRegex.Match(immediate)).Success)
			{
                s = m.Groups["Value"].Value;
                if (s.Length > 8)
                    throw new NotImplementedException("Values longer than 4 bytes not supported.");
                uint val = uint.Parse(s, System.Globalization.NumberStyles.HexNumber);
                var data = BitConverter.GetBytes(val);
				yield return EmitInstruction(data);
			}
            else if ((m = DecRegex.Match(immediate)).Success)
            {
                s = m.Groups["Value"].Value;
                int val = int.Parse(s);
                var data = BitConverter.GetBytes(val);
				yield return EmitInstruction(data);
            }
            else
            {
                throw new SyntaxErrorException("Unrecognized data format", 0, 0);
            }
		}
		/// <summary>
		/// Parses a register string
		/// </summary>
		/// <param name="r">Register string to parse</param>
		/// <param name="line">Debug line</param>
		/// <returns></returns>
		protected static Register ParseRegister(string r, int line)
		{
			r = r.TrimStart('$');
			int reg = 0;
			if (int.TryParse(r, out reg))
				return (Register)reg;
			else
			{
				try
				{
					Register res = (Register)Enum.Parse(typeof(Register), r);
					return res;
				}
				catch (ArgumentException)
				{
					throw new SyntaxErrorException("Unkown register '" + reg + "'", line, 0);
				}
			}

		}

		/// <summary>
		/// Compiles a binary program from a meta-program
		/// </summary>
		/// <param name="meta_program">The parsed program to compile</param>
		/// <returns>A binary code program compiled from the meta-program</returns>
		public static MipsProgram Compile(List<MipsMetaInstruction> meta_program, Action<CompilerLogEntry> callback)
		{
			MipsProgram prog = new MipsProgram();
			Dictionary<string, int> labels = new Dictionary<string, int>();
			// Scan for labels and calculate their values.
			for (int i = 0; i < meta_program.Count; i++)
			{
				var item = meta_program[i];
				if (!string.IsNullOrEmpty(item.Label))
				{
					// Mips instructions are four-byte aligned
					labels.Add(item.Label, i * 4);
				}
			}
			foreach (var item in meta_program)
			{
				MipsInstruction ins = new MipsInstruction();
				ins.OpCode = item.OpCode;
				ushort parse = 0;
				ins.Funct = item.Funct;
				// Check instruction type, and set the appropriate field
				if (item.OpCode == OpCode.Data)
				{
					ins = new MipsInstruction(BitConverter.ToUInt32(item.RawData, 0));
				}
				else
					switch (ins.Type)
					{
						case InstructionType.RType:

							// Syscall should not take any parameters
							if (ins.Funct == Function.JR)
							{
								if (string.IsNullOrEmpty(item.Rd))
								{
									callback(new CompilerLogEntry("Expected register", item.File, item.Line, 0, CompilerLogEntryType.Error));
								}
								if (!(string.IsNullOrEmpty(item.Rs) || string.IsNullOrEmpty(item.Rt) || string.IsNullOrEmpty(item.Immediate)))
									callback(new CompilerLogEntry("Unexpected arguments", item.File, item.Line, 0, CompilerLogEntryType.Error));
								ins.Rt = ParseRegister(item.Rd, item.Line);
							}
							else if (ins.Funct != Function.SYSCALL)
							{
								// Check if required fields are defined
								if ((string.IsNullOrEmpty(item.Rs) || string.IsNullOrEmpty(item.Rt)) && callback != null)
									callback(new CompilerLogEntry("Expected register", item.File, item.Line, 0, CompilerLogEntryType.Error));
								else
								{

									if (string.IsNullOrEmpty(item.Immediate))
									{
										ins.Rs = ParseRegister(item.Rs, item.Line);
										ins.Rt = ParseRegister(item.Rt, item.Line);
										ins.Rd = ParseRegister(item.Rd, item.Line);
									}
									else
									{
										ins.Rt = ParseRegister(item.Rd, item.Line);
										ins.Rs = ParseRegister(item.Rs, item.Line);
										ins.Shamt = uint.Parse(item.Immediate);
									}
								}
							}

							break;
						case InstructionType.IType:
							// Check if required fields are defined
							if ((string.IsNullOrEmpty(item.Rd) || string.IsNullOrEmpty(item.Rs)) && callback != null)
								callback(new CompilerLogEntry("Expected register", item.File, item.Line, 0, CompilerLogEntryType.Error));
							else
							{
							// NOTE: Register order swapped
							ins.Rs = ParseRegister(item.Rs, item.Line);
							ins.Rt = ParseRegister(item.Rd, item.Line);
							if (labels.ContainsKey(item.Immediate)) {
								// BNE error origin.
                                if (ins.OpCode == OpCode.BEQ || ins.OpCode == OpCode.BNE || ins.OpCode == OpCode.BLTZ)
                                {
                                    uint labelValue = (uint)labels[item.Immediate] / 4;
                                    uint line = (uint)prog.instructions.Count;
                                    ins.Immediate = (uint)(labelValue - line);//line / 4;
                                }
								else ins.Immediate = (uint)labels[item.Immediate]; // Old version
							}
							else if (item.Immediate.StartsWith("0x", StringComparison.InvariantCultureIgnoreCase))
								ins.Immediate = ushort.Parse(item.Immediate.Substring(2), System.Globalization.NumberStyles.HexNumber);
							else if (ushort.TryParse(item.Immediate, out parse))
								ins.Immediate = parse;
							else
								callback(new CompilerLogEntry("Expected constant", item.File, item.Line, 0, CompilerLogEntryType.Error));
							}
							break;
						case InstructionType.JType:
							// Check if jump address is provided
							if (string.IsNullOrEmpty(item.Immediate))
								callback(new CompilerLogEntry("Expected jump address", item.File, item.Line, 0, CompilerLogEntryType.Error));
							else
							{
								if (labels.ContainsKey(item.Immediate)) {
									ins.Address = (uint)labels[item.Immediate]/4; // Jump fixed. Added /4. Jump should not be address. Only line.
								}
								else if (item.Immediate.StartsWith("0x", StringComparison.InvariantCultureIgnoreCase))
									ins.Address = uint.Parse(item.Immediate.Substring(2), System.Globalization.NumberStyles.HexNumber);
								else {
									uint adr = 0;
									if (!uint.TryParse(item.Immediate, out adr))
										callback(new CompilerLogEntry("Unable to recognize '" + item.Immediate + "' as a valid immediate value", item.File, item.Line, 0, CompilerLogEntryType.Error));
									else
										ins.Address = adr;

								}
							}
							break;
					}
				prog.instructions.Add(ins);
			}
			return prog;
		}
	}
}
